﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using Borg.Properties;

namespace Borg
{
    [Serializable]
    public sealed class TypeBuilder
    {
        private int _nextContractId;
        private int _nextSlotDefinitionId;
        private int _nextParamDefinitionId;
        private int _nextTagDefinitionId;
        private int _nextPluginId;
        private int _nextExtensionTypeId;
        private int _nextPlugTypeId;
        private int _nextSlotTypeId;
        private int _nextParamId;

        private Runtime _runtime;

        public Runtime Runtime
        {
            get { return _runtime; }
            private set { _runtime = value; }
        }

        internal TypeBuilder(Runtime runtime)
        {
            Runtime = runtime;
        }

        public Contract CreateContract(string name, Assembly assembly,
            ICollection<SlotDefinition> slotDefinitions, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            return InternalCreateContract(NextContractId(), name, assembly,
                Runtime.EnvironmentUri, slotDefinitions, analyzerId);
        }

        public Contract CreateContract(string name, Uri originUri, Assembly assembly,
            ICollection<SlotDefinition> slotDefinitions, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (originUri == null)
                throw new ArgumentNullException("originUri");

            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            return InternalCreateContract(NextContractId(), name, originUri, assembly,
                Runtime.EnvironmentUri, slotDefinitions, analyzerId);
        }

        public Contract CreateContract(string name, Uri originUri, Uri assemblyUri,
            ICollection<SlotDefinition> slotDefinitions, string analyzerId = "")
        {
            if (name == null)

                throw new ArgumentNullException("name");
            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (originUri == null)
                throw new ArgumentNullException("originUri");

            if (assemblyUri == null)
                throw new ArgumentNullException("assemblyUri");
            
            return InternalCreateContract(NextContractId(), name, originUri,
                Runtime.EnvironmentUri, assemblyUri, slotDefinitions, analyzerId);
        }

        public Contract CreateContract(string name, Assembly assembly, Uri environmentUri,
            ICollection<SlotDefinition> slotDefinitions, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");
            
            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");
            
            if (assembly == null)
                throw new ArgumentNullException("assembly");
            if (environmentUri == null)
                throw new ArgumentNullException("environmentUri");
            
            return InternalCreateContract(NextContractId(), name, assembly,
                environmentUri, slotDefinitions, analyzerId);
        }

        public Contract CreateContract(string name, Uri originUri, Assembly assembly,
            Uri environmentUri, ICollection<SlotDefinition> slotDefinitions, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");
            
            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");
            
            if (originUri == null)
                throw new ArgumentNullException("originUri");
            
            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            if (environmentUri == null)
                throw new ArgumentNullException("environmentUri");
            
            return InternalCreateContract(NextContractId(), name, originUri, assembly,
                environmentUri, slotDefinitions, analyzerId);
        }

        public Contract CreateContract(string name, Uri originUri, Uri assemblyUri,
            Uri environmentUri, ICollection<SlotDefinition> slotDefinitions, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");
            
            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");
            
            if (originUri == null)
                throw new ArgumentNullException("originUri");
            
            if (assemblyUri == null)
                throw new ArgumentNullException("assemblyUri");
            
            if (environmentUri == null)
                throw new ArgumentNullException("environmentUri");
            
            return InternalCreateContract(NextContractId(), name, originUri, assemblyUri,
                    environmentUri, slotDefinitions, analyzerId);
        }

        public SlotDefinition CreateSlotDefinition(SlotDefinitionAttribute attribute, string typeName,
            ICollection<TagDefinition> tagDefinitions, ICollection<ParamDefinition> paramDefinitions)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");

            if (typeName == null)
                throw new ArgumentNullException("typeName");

            if (typeName == string.Empty)
                throw new ArgumentException(Exceptions.Type_name_must_not_be_empty, "typeName");

            if (string.IsNullOrEmpty(attribute.Name))
            {
                var index = typeName.LastIndexOf('.');

                attribute.Name =
                    index < 0
                        ? typeName
                        : typeName.Substring(index + 1);
            }

            return InternalCreateSlotDefinition(NextSlotDefinitionId(),
                attribute, typeName, tagDefinitions, paramDefinitions);
        }

        public SlotDefinition CreateSlotDefinition(SlotDefinitionAttribute attribute, Type type,
            ICollection<TagDefinition> tagDefinitions, ICollection<ParamDefinition> paramDefinitions)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");

            if (type == null)
                throw new ArgumentNullException("type");

            if (string.IsNullOrEmpty(attribute.Name))
                attribute.Name = type.Name;
            
            return InternalCreateSlotDefinition(NextSlotDefinitionId(),
                attribute, type.FullName, tagDefinitions, paramDefinitions);
        }

        public ParamDefinition CreateParamDefinition(ParamDefinitionAttribute attribute)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreateParamDefinition(NextParamDefinitionId(), attribute);
        }

        public TagDefinition[] CreateTagDefinitions(TagDefinitionAttribute attribute)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreateTagDefinitions(attribute);
        }

        public Plugin CreatePlugin(string name, Assembly assembly,
            ICollection<ExtensionType> extensionTypes, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            return InternalCreatePlugin(NextPluginId(), name, assembly,
                Runtime.EnvironmentUri, extensionTypes, analyzerId);
        }

        public Plugin CreatePlugin(string name, Uri originUri, Assembly assembly,
            ICollection<ExtensionType> extensionTypes, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (originUri == null)
                throw new ArgumentNullException("originUri");

            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            return InternalCreatePlugin(NextPluginId(), name, originUri, assembly,
                Runtime.EnvironmentUri, extensionTypes, analyzerId);
        }

        public Plugin CreatePlugin(string name, Uri originUri, Uri assemblyUri,
            ICollection<ExtensionType> extensionTypes, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (originUri == null)
                throw new ArgumentNullException("originUri");

            if (assemblyUri == null)
                throw new ArgumentNullException("assemblyUri");
            
            return InternalCreatePlugin(NextPluginId(), name, originUri, assemblyUri,
                Runtime.EnvironmentUri, extensionTypes, analyzerId);
        }

        public Plugin CreatePlugin(string name, Assembly assembly, Uri environmentUri,
            ICollection<ExtensionType> extensionTypes, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (assembly == null)
                throw new ArgumentNullException("assembly");

            if (environmentUri == null)
                throw new ArgumentNullException("environmentUri");
            
            return InternalCreatePlugin(NextPluginId(), name, assembly,
                environmentUri, extensionTypes, analyzerId);
        }

        public Plugin CreatePlugin(string name, Uri originUri, Assembly assembly,
            Uri environmentUri, ICollection<ExtensionType> extensionTypes, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");
            
            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");
            
            if (originUri == null)
                throw new ArgumentNullException("originUri");
            
            if (assembly == null)
                throw new ArgumentNullException("assembly");
            
            if (environmentUri == null)
                throw new ArgumentNullException("environmentUri");
            
            return InternalCreatePlugin(NextPluginId(), name, originUri, assembly,
                environmentUri, extensionTypes, analyzerId);
        }

        public Plugin CreatePlugin(string name, Uri originUri, Uri assemblyUri,
            Uri environmentUri, ICollection<ExtensionType> extensionTypes, string analyzerId = "")
        {
            if (name == null)
                throw new ArgumentNullException("name");

            if (name == string.Empty)
                throw new ArgumentException(Exceptions.Name_must_not_be_empty, "name");

            if (originUri == null)
                throw new ArgumentNullException("originUri");

            if (assemblyUri == null)
                throw new ArgumentNullException("assemblyUri");

            if (environmentUri == null)
                throw new ArgumentNullException("environmentUri");
            
            return InternalCreatePlugin(NextPluginId(), name, originUri,
                assemblyUri, environmentUri, extensionTypes, analyzerId);
        }

        public ExtensionType CreateExtensionType(ExtensionAttribute attribute, string typeName,
            ICollection<PlugType> plugTypes, ICollection<SlotType> slotTypes, bool isTransparent = false)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            if (typeName == null)
                throw new ArgumentNullException("typeName");
            
            if (typeName == string.Empty)
                throw new ArgumentException(Exceptions.Type_name_must_not_be_empty, "typeName");
            
            if (string.IsNullOrEmpty(attribute.Name))
            {
                var index = typeName.LastIndexOf('.');

                attribute.Name =
                    index < 0
                        ? typeName
                        : typeName.Substring(index + 1);
            }

            return InternalCreateExtensionType(NextExtensionTypeId(),
                attribute, typeName, plugTypes, slotTypes, isTransparent);
        }

        public ExtensionType CreateExtensionType(ExtensionAttribute attribute, Type type,
            ICollection<PlugType> plugTypes, ICollection<SlotType> slotTypes, bool isTransparent = false)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            if (type == null)
                throw new ArgumentNullException("type");
            
            if (string.IsNullOrEmpty(attribute.Name))
                attribute.Name = type.Name;
            
            return InternalCreateExtensionType(NextExtensionTypeId(),
                attribute, type.FullName, plugTypes, slotTypes, isTransparent);
        }

        public PlugType CreatePlugType(PlugAttribute attribute, IEnumerable<Param> parameters)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreatePlugType(NextPlugTypeId(), attribute, parameters);
        }

        public Param CreateParam(ParamAttribute attribute)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreateParam(NextParamId(), attribute);
        }

        public SlotType CreateSlotType(SlotAttribute attribute)
        {
            if (attribute == null)
                throw new ArgumentNullException("attribute");
            
            return InternalCreateSlotType(NextSlotTypeId(), attribute);
        }

        private Contract InternalCreateContract(int id, string name, Assembly assembly,
            Uri environmentUri, IEnumerable<SlotDefinition> slotDefinitions, string analyzerId)
        {
            return InternalCreateContract(id, name, GetOriginUri(assembly),
                assembly, environmentUri, slotDefinitions, analyzerId);
        }

        private Contract InternalCreateContract(int id, string name, Uri originUri, Assembly assembly,
            Uri environmentUri, IEnumerable<SlotDefinition> slotDefinitions, string analyzerId)
        {
            var contract = new Contract(analyzerId, Runtime, id, name,
                originUri, assembly, environmentUri);

            InternalSetSlotDefinitions(contract, slotDefinitions);
            
            return contract;
        }

        private Contract InternalCreateContract(int id, string name, Uri originUri, Uri assemblyUri,
            Uri environmentUri, IEnumerable<SlotDefinition> slotDefinitions, string analyzerId)
        {
            var contract = new Contract(analyzerId, Runtime, id, name,
                originUri, assemblyUri, environmentUri);
            
            InternalSetSlotDefinitions(contract, slotDefinitions);
            
            return contract;
        }

        private static void InternalSetSlotDefinitions(Contract contract,
            IEnumerable<SlotDefinition> slotDefinitions)
        {
            contract.SlotDefinitions =
                new MetaElementCollection<SlotDefinition>();

            if (slotDefinitions == null)
                return;

            var hashSet = new HashSet<string>();
            
            foreach (var slotDef in slotDefinitions)
            {
                if (hashSet.Contains(slotDef.Name))
                    throw new ArgumentException(
                        Exceptions.TypeBuilder_SlotDefinitionNameNotUnique, "slotDefinitions");
                
                hashSet.Add(slotDef.Name);

                if (slotDef.Contract != null)
                    throw new InvalidOperationException(
                        "SlotDefinition is already assigned to a Contract");

                slotDef.Contract = contract;
                contract.SlotDefinitions.Add(slotDef);
            }
        }

        private static SlotDefinition InternalCreateSlotDefinition(int id,
            SlotDefinitionAttribute attribute, string typeName, IEnumerable<TagDefinition> tagDefinitions,
            IEnumerable<ParamDefinition> paramDefinitions)
        {
            var slotDefinition =
                new SlotDefinition(id, attribute.Name, attribute.IsThreadsafe)
                    {
                        TypeName = typeName,
                        TagDefinitions = new MetaElementCollection<TagDefinition>()
                    };

            if (tagDefinitions != null)
            {
                var hashSet = new HashSet<string>();
                
                foreach (var tagDef in tagDefinitions)
                {
                    if (hashSet.Contains(tagDef.Name))
                        throw new ArgumentException(
                            Exceptions.TypeBuilder_TagDefinitionNameNotUnique, "tagDefinitions");
                    
                    hashSet.Add(tagDef.Name);
                    
                    if (tagDef.SlotDefinition != null)
                        throw new InvalidOperationException(
                            "TagDefinition is already assigned to a SlotDefinition");
                    
                    tagDef.SlotDefinition = slotDefinition;
                    slotDefinition.TagDefinitions.Add(tagDef);
                }
            }

            slotDefinition.ParamDefinitions = new MetaElementCollection<ParamDefinition>();
            
            if (paramDefinitions != null)
            {
                HashSet<string> hashSet = new HashSet<string>();
                foreach (ParamDefinition element in paramDefinitions)
                {
                    if (hashSet.Contains(element.Name))
                        throw new ArgumentException("ParamDefinition name not unique", "paramDefinitions");
                    hashSet.Add(element.Name);
                    if (element.SlotDefinition != null)
                        throw new InvalidOperationException("ParamDefinition is already assigned to a SlotDefinition");
                    element.SlotDefinition = slotDefinition;
                    slotDefinition.ParamDefinitions.Add(element);
                }
            }
            return slotDefinition;
        }

        private ParamDefinition InternalCreateParamDefinition(int id, ParamDefinitionAttribute attribute)
        {
            var paramDefinition = new ParamDefinition(id, attribute.Name)
            {
                TypeName = attribute.TypeName
            };

            if (attribute.DefaultValue != ParamDefinition.NoDefault)
                paramDefinition.DefaultParam =
                    new Param(NextParamId(), attribute.Name)
                        {
                            Value = attribute.DefaultValue
                        };
            
            return paramDefinition;
        }

        private TagDefinition[] InternalCreateTagDefinitions(TagDefinitionAttribute attribute)
        {
            var tagDefs = new TagDefinition[attribute.Tags.Length];

            for (var i = 0; i < tagDefs.Length; ++i)
                tagDefs[i] = new TagDefinition(NextTagDefinitionId(), attribute.Tags[i].Trim());
            
            return tagDefs;
        }

        private Plugin InternalCreatePlugin(int id, string name, Uri originUri, Uri assemblyUri,
            Uri environmentUri, IEnumerable<ExtensionType> extensionTypes, string analyzerId)
        {
            var plugin = new Plugin(analyzerId, Runtime, id, name,
                originUri, assemblyUri, environmentUri);
            
            InternalSetExtensionTypes(plugin, extensionTypes);
            
            return plugin;
        }

        private Plugin InternalCreatePlugin(int id, string name, Assembly assembly, Uri environmentUri,
            IEnumerable<ExtensionType> extensionTypes, string analyzerId)
        {
            return InternalCreatePlugin(id, name, GetOriginUri(assembly),
                assembly, environmentUri, extensionTypes, analyzerId);
        }

        private Plugin InternalCreatePlugin(int id, string name, Uri originUri, Assembly assembly,
            Uri environmentUri, IEnumerable<ExtensionType> extensionTypes, string analyzerId)
        {
            var plugin = new Plugin(analyzerId, Runtime, id, name,
                originUri, assembly, environmentUri);
            
            InternalSetExtensionTypes(plugin, extensionTypes);
            
            return plugin;
        }

        private static void InternalSetExtensionTypes(Plugin plugin, IEnumerable<ExtensionType> extensionTypes)
        {
            plugin.ExtensionTypes = new MetaElementCollection<ExtensionType>();
            
            if (extensionTypes == null)
                return;
            
            var hashSet = new HashSet<string>();

            foreach (ExtensionType element in extensionTypes)
            {
                if (hashSet.Contains(element.Name))
                    throw new ArgumentException("ExtensionType name not unique", "extensionTypes");

                hashSet.Add(element.Name);

                if (element.Plugin != null)
                    throw new InvalidOperationException("ExtensionType is already assigned to a Plugin");
                
                element.Plugin = plugin;
                plugin.ExtensionTypes.Add(element);
            }
        }

        private static ExtensionType InternalCreateExtensionType(int id, ExtensionAttribute attribute,
            string typeName, IEnumerable<PlugType> plugTypes, IEnumerable<SlotType> slotTypes, bool isTransparent)
        {
            var extensionType =
                new ExtensionType(id, attribute.Name)
                    {
                        TypeName = typeName,
                        ActivationMode = attribute.ActivationMode,
                        IsTransparent = isTransparent,
                        OnActivated = attribute.OnActivated,
                        OnDeactivated = attribute.OnDeactivated,
                        PlugTypes = new MetaElementCollection<PlugType>()
                    };

            if (plugTypes != null)
            {
                var hashSet = new HashSet<string>();
                
                foreach (var plug in plugTypes)
                {
                    if (hashSet.Contains(plug.Name))
                        throw new ArgumentException("PlugType name not unique", "plugTypes");
                    
                    hashSet.Add(plug.Name);
                    
                    if (plug.ExtensionType != null)
                        throw new InvalidOperationException("PlugType is already assigned to a ExtensionType");
                    
                    plug.ExtensionType = extensionType;
                    extensionType.PlugTypes.Add(plug);
                }
            }

            extensionType.SlotTypes = new MetaElementCollection<SlotType>();
            
            if (slotTypes != null)
            {
                var hashSet = new HashSet<string>();
                
                foreach (var slot in slotTypes)
                {
                    if (hashSet.Contains(slot.Name))
                        throw new ArgumentException("SlotType name not unique", "slotTypes");
                    
                    hashSet.Add(slot.Name);
                    
                    if (slot.ExtensionType != null)
                        throw new InvalidOperationException("SlotType is already assigned to a ExtensionType");
                    
                    slot.ExtensionType = extensionType;
                    extensionType.SlotTypes.Add(slot);
                }
            }

            return extensionType;
        }

        private static PlugType InternalCreatePlugType(int id, PlugAttribute attribute, IEnumerable<Param> parameters)
        {
            var plugType =
                new PlugType(id, attribute.Name)
                    {
                        AutoPlug = attribute.AutoPlug,
                        AutoTag = GetAutoTag(attribute.AutoTag),
                        OnPlugging = attribute.OnPlugging,
                        OnPlugged = attribute.OnPlugged,
                        OnUnplugging = attribute.OnUnplugging,
                        OnUnplugged = attribute.OnUnplugged,
                        OnTagging = attribute.OnTagging,
                        OnTagged = attribute.OnTagged,
                        OnUntagging = attribute.OnUntagging,
                        OnUntagged = attribute.OnUntagged,
                        Params = new MetaElementCollection<Param>()
                    };

            if (parameters != null)
            {
                var hashSet = new HashSet<string>();
                
                foreach (var parameter in parameters)
                {
                    if (hashSet.Contains(parameter.Name))
                        throw new ArgumentException("Parameter name not unique", "parameters");

                    hashSet.Add(parameter.Name);

                    if (parameter.PlugType != null)
                        throw new InvalidOperationException("Parameter is already assigned to a PlugType");

                    parameter.PlugType = plugType;
                    plugType.Params.Add(parameter);
                }
            }

            return plugType;
        }

        private static Param InternalCreateParam(int id, ParamAttribute attribute)
        {
            return new Param(id, attribute.Name)
            {
                Value = attribute.Value
            };
        }

        private static SlotType InternalCreateSlotType(int id, SlotAttribute attribute)
        {
            return new SlotType(id, attribute.Name)
            {
                AutoOpen = attribute.AutoOpen,
                AutoPlug = attribute.AutoPlug,
                AutoTag = GetAutoTag(attribute.AutoTag),
                Shared = attribute.Shared,
                OnOpening = attribute.OnOpening,
                OnOpened = attribute.OnOpened,
                OnClosing = attribute.OnClosing,
                OnClosed = attribute.OnClosed,
                OnPlugging = attribute.OnPlugging,
                OnPlugged = attribute.OnPlugged,
                OnUnplugging = attribute.OnUnplugging,
                OnUnplugged = attribute.OnUnplugged,
                OnTagging = attribute.OnTagging,
                OnTagged = attribute.OnTagged,
                OnUntagging = attribute.OnUntagging,
                OnUntagged = attribute.OnUntagged
            };
        }

        private static string[] GetAutoTag(string tagString)
        {
            if (string.IsNullOrEmpty(tagString))
                return new string[0];

            var s = tagString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            
            for (var i = 0; i < s.Length; ++i)
                s[i] = s[i].Trim();
            
            return s;
        }

        public static Uri GetOriginUri(Assembly assembly)
        {
            return
                new Uri(
                    assembly.IsDynamic
                        ? string.Format("memory://dynamic.generated/{0}", assembly.GetName().Name)
                        : assembly.EscapedCodeBase);
        }

        private int NextContractId()
        {
            return Interlocked.Increment(ref _nextContractId);
        }

        private int NextSlotDefinitionId()
        {
            return Interlocked.Increment(ref _nextSlotDefinitionId);
        }

        private int NextParamDefinitionId()
        {
            return Interlocked.Increment(ref _nextParamDefinitionId);
        }

        private int NextTagDefinitionId()
        {
            return Interlocked.Increment(ref _nextTagDefinitionId);
        }

        private int NextPluginId()
        {
            return Interlocked.Increment(ref _nextPluginId);
        }

        private int NextExtensionTypeId()
        {
            return Interlocked.Increment(ref _nextExtensionTypeId);
        }

        private int NextPlugTypeId()
        {
            return Interlocked.Increment(ref _nextPlugTypeId);
        }

        private int NextParamId()
        {
            return Interlocked.Increment(ref _nextParamId);
        }

        private int NextSlotTypeId()
        {
            return Interlocked.Increment(ref _nextSlotTypeId);
        }
    }
}
